var _ = require('underscore');
var keystone = require('../');

/**
 * UpdateHandler Class
 *
 * @param {Object} item to update
 * @api public
 */

function UpdateHandler(list, item, req, res, options) {

	if (!(this instanceof UpdateHandler)) {
		return new UpdateHandler(list, item);
	}

	this.list = list;
	this.item = item;
	this.req = req;
	this.res = res;
	this.user = req.user;
	this.options = options || {};

	if (!this.options.errorMessage) {
		this.options.errorMessage = 'There was a problem saving your changes:';
	}

	if (this.options.user) {
		this.user = this.options.user;
	}

	this.validationMethods = {};
	this.validationErrors = {};

}


/**
 * Adds a custom validation method for a given path
 *
 * @param {string} path to call method for
 * @param {function} method to call
 * @api public
 */

UpdateHandler.prototype.validate = function(path, fn) {
	this.validationMethods[path] = fn;
	return this;
};


/**
 * Adds a validationError to the updateHandler; can be used before
 * `.process()` is called to handle errors generated by custom pre-
 * processing.
 *
 * @param {string} path that failed validation
 * @param {string} message to display
 * @param {string} error type (defaults to 'required')
 * @api public
 */
UpdateHandler.ValidationErrorName = 'ValidatorError';
UpdateHandler.prototype.addValidationError = function(path, msg, type) {
	this.validationErrors[path] = {
		name: UpdateHandler.ValidationErrorName,
		path: path,
		message: msg,
		type: type || 'required'
	};
	return this;
};


/**
 * Processes data from req.body, req.query, or any data source.
 *
 * Options:
 * - fields (comma-delimited list or array of field paths)
 * - flashErrors (boolean, default false; whether to push validation errors to req.flash)
 * - ignoreNoedit (boolean, default false; whether to ignore noedit settings on fields)
 * - validationErrors (object; validation errors from previous form handling that should be included)
 *
 * @param {Object} data
 * @param {Object} options (can be comma-delimited list of fields) (optional)
 * @param {Function} callback (optional)
 * @api public
 */

UpdateHandler.prototype.process = function(data, options, callback) {
	
	var usingDefaultFields = false;
	
	if ('function' === typeof options) {
		callback = options;
		options = null;
	}

	if ('function' !== typeof callback) {
		callback = function() {};
	}
	
	// Initialise options
	
	if (!options) {
		options = {};
	} else if ('string' === typeof options) {
		options = { fields: options };
	}
	
	if (!options.fields) {
		options.fields = _.keys(this.list.fields);
		usingDefaultFields = true;
	} else if ('string' === typeof options.fields) {
		options.fields = options.fields.split(',').map(function(i) { return i.trim(); });
	}
	
	options.required = options.required || {};
	options.errorMessage = options.errorMessage || this.options.errorMessage;
	options.invalidMessages = options.invalidMessages || {};
	options.requiredMessages = options.requiredMessages || {};
	
	// Parse a string of required fields into field paths
	if ('string' === typeof options.required) {
		var requiredFields = options.required.split(',').map(function(i) { return i.trim(); });
		options.required = {};
		requiredFields.forEach(function(path) {
			options.required[path] = true;
		});
	}
	
	// Make sure fields with the required option set are included in the required paths
	options.fields.forEach(function(path) {
		var field = (path instanceof keystone.Field) ? path : this.list.field(path);
		if (field && field.required) {
			options.required[field.path] = true;
		}
	}, this);
	
	// TODO: The whole progress queue management code could be a lot neater...
	var actionQueue = [],
		addValidationError = this.addValidationError.bind(this),
		validationErrors = this.validationErrors;

	var progress = function(err) {
		if (err) {
			if (options.logErrors) {
				console.log('Error saving changes to ' + this.item.list.singular + ' ' + this.item.id + ':');
				console.log(err);
			}
			callback(err, this.item, this);
		} else if (_.size(validationErrors)) {
			if (options.flashErrors) {
				this.req.flash('error', {
					type: UpdateHandler.ValidationErrorName,
					title: options.errorMessage,
					list: _.pluck(validationErrors, 'message')
				});
			}
			callback({
				message: 'Validation failed',
				name: UpdateHandler.ValidationErrorName,
				errors: validationErrors
			}, this.item, this);
		} else if (actionQueue.length) {
			// TODO: parallel queue handling for cloudinary uploads?
			actionQueue.pop()();
		} else {
			saveItem();
		}
	}.bind(this);
	
	var saveItem = function() {
		
		// Make current user available to pre/post save events
		this.item._req_user = this.user;
		
		this.item.save(function(err) {
			if (err) {
				if (err.name === UpdateHandler.ValidationErrorName) {
					// don't log simple validation errors
					if (options.flashErrors) {
						this.req.flash('error', {
							type: UpdateHandler.ValidationErrorName,
							title: options.errorMessage,
							list: _.pluck(err.errors, 'message')
						});
					}
				} else {
					if (options.logErrors) {
						console.log('Error saving changes to ' + this.item.list.singular + ' ' + this.item.id + ':');
						console.log(err);
					}
					if (options.flashErrors) {
						this.req.flash('error', 'There was an error saving your changes: ' + err.message + ' (' + err.name + (err.type ? ': ' + err.type : '') + ')');
					}
				}
				addValidationError(err.path, err.message, err.type);

				err = {
					message: 'Validation failed',
					name: UpdateHandler.ValidationErrorName,
					errors: validationErrors
				};
			}

			return callback(err, this.item, this);
		}.bind(this));
	}.bind(this);
	
	options.fields.forEach(function(path) {
		
		// console.log('Processing field ' + path);
		var message;
		
		var field = (path instanceof keystone.Field) ? path : this.list.field(path),
			invalidated = false;
		
		if (!field) {
			throw new Error('UpdateHandler.process called with invalid field path: ' + path);
		}
		
		// skip uneditable fields
		if (usingDefaultFields && field.noedit && !options.ignoreNoedit) {
			// console.log('Skipping field ' + path + ' (noedit: true)');
			return;
		}
		
		// Some field types have custom behaviours for queueing or validation
		switch (field.type) {
			
			case 'localfile':
			case 'localfiles':
			case 'cloudinaryimage':
			case 'cloudinaryimages':
			case 'azurefile':
			case 's3file':
				actionQueue.push(field.getRequestHandler(this.item, this.req, options.paths, function(err) {
					if (err && options.flashErrors) {
						this.req.flash('error', field.label + ' upload failed - ' + err.message);
					}
					progress(err);
				}.bind(this)));
			break;
			
			case 'location':
				actionQueue.push(field.getRequestHandler(this.item, this.req, options.paths, function(err) {
					if (err && options.flashErrors) {
						this.req.flash('error', field.label + ' improve failed - ' + (err.status_text || err.status));
					}
					progress(err);
				}.bind(this)));
			break;
			
			case 'password':
				// passwords should only be set if a value is provided.
				// if no value is provided, as long as the field isn't required or empty, bail.
				if (!data[field.path] && (!options.required[field.path] || this.item.get(field.path))) {
					return;
				}
				// validate the password fields match, with a custom error message.
				if (data[field.path] !== data[field.paths.confirm]) {
					message = options.invalidMessages[field.path + '_match'] || 'Passwords must match';
					addValidationError(field.path, message);
					invalidated = true;
				}
			break;
			
		}
		
		// validate field input, unless it's already been invalidated by field-specific behaviour
		if (!invalidated && !field.validateInput(data)) {
			// console.log('Field ' + field.path + ' is invalid');
			message = options.invalidMessages[field.path] || field.options.invalidMessage || 'Please enter a valid ' + field.typeDescription + ' in the ' + field.label + ' field';
			addValidationError(field.path, message);
			invalidated = true;
		}
		
		// validate required fields, unless they've already been invalidated by field-specific behaviour
		if (!invalidated && options.required[field.path] && !field.validateInput(data, true, this.item) && !validationErrors[field.path]) {
			// console.log('Field ' + field.path + ' is required, but not provided.');
			message = options.requiredMessages[field.path] || field.options.requiredMessage || field.label + ' is required';
			addValidationError(field.path, message);
			invalidated = true;
		}
		
		// check for a custom validation rule at the path, and run it (unless the field is already invalid)
		if (!invalidated && this.validationMethods[field.path]) {
			message = this.validationMethods[field.path](data);
			if (message) {
				addValidationError(field.path, message);
			}
			invalidated = true;
		}
		
		field.updateItem(this.item, data);
		
	}, this);

	progress();
	
};


/*!
 * Export class
 */

exports = module.exports = UpdateHandler;
